Skip to main content

Custom Thumbnail Bar

You can use the <cylindo-viewer> with a custom thumbnail bar. Set the value of the slot attribute of the custom thumbnail bar to thumbnail-bar and use the methods and properties on the <cylindo-viewer> to control the thumbnail bar.

Simple custom thumbnail bar

Below, you will find a simple example to get you started.

info

This interactive example demonstrates the concept using React. If you're using a different framework, adapt the core concepts to your preferred technology.

Result
Loading...
Live Editor
function Example() {
  const viewerRef = React.useRef(null);
  const [viewerItemIndex, setViewerItemIndex] = useState(0);
  const [items, setItems] = useState([]);

  useEffect(() => {
    const viewer = viewerRef.current;
    if (!viewer) return;

    const onLocalItemsChange = event => {
      setItems(event.detail);
    };

    const onItemChange = event => {
      setViewerItemIndex(event.detail.index);
    };

    viewer.addEventListener("items-change", onLocalItemsChange);
    viewer.addEventListener("item-change", onItemChange);
    return () => {
      viewer.removeEventListener("items-change", onLocalItemsChange);
      viewer.removeEventListener("item-change", onItemChange);
    };
  }, []);

  return (
    <cylindo-viewer
      ref={viewerRef}
      customer-id="5098"
      code="ARMCHAIR-PDP"
    >
      <cylindo-studio code="RS-BARILA-A" customer-id="5098" />
      <cylindo-360-frame frame="10" />
      <cylindo-360-frame frame="16" />
      <cylindo-360 frame="3" />
      <cylindo-model />
      <div slot="thumbnail-bar" className="thumbnail-bar">
        {items &&
          items.map((item, index) => {
            const isSelected = viewerItemIndex === index;
            {
              /* Button where you can pass your own design,
                 or you can call the Content API (CAPI) to get content for each item. */
            }
            return (
              <button
                key={index}
                className={isSelected ? "selected" : ""}
                onClick={() =>
                  (viewerRef.current.item = {
                    viewerItem: item,
                    viewerItemIndex: index,
                  })
                }
              >
                {item.type}
              </button>
            );
          })}
      </div>
    </cylindo-viewer>
  );
}

Custom thumbnail bar with remote config

The following example demonstrates how to build a thumbnail bar for remote config.

info

This interactive example demonstrates the concept using React. If you're using a different framework, adapt the core concepts to your preferred technology.

Result
Loading...
Live Editor
function Example() {
  const viewerRef = React.useRef(null);
  const [viewerItemIndex, setViewerItemIndex] = useState(0);
  const [localItems, setLocalItems] = useState([]);
  const [configItems, setConfigItems] = useState([]);

  useEffect(() => {
    const viewer = viewerRef.current;
    if (!viewer) return;

    const onLocalItemsChange = event => {
      setLocalItems(event.detail);
    };

    const onRemoteItemsChange = event => {
      setConfigItems(event.detail.items);
    };

    const onItemChange = event => {
      setViewerItemIndex(event.detail.index);
    };

    viewer.addEventListener("items-change", onLocalItemsChange);
    viewer.addEventListener("config-change", onRemoteItemsChange);
    viewer.addEventListener("item-change", onItemChange);
    return () => {
      viewer.removeEventListener("items-change", onLocalItemsChange);
      viewer.removeEventListener("config-change", onRemoteItemsChange);
      viewer.removeEventListener("item-change", onItemChange);
    };
  }, []);

  const items = [...configItems, ...localItems];

  return (
    <cylindo-viewer
      ref={viewerRef}
      customer-id="5098"
      code="SALSIE FF"
      remote-config="k2hctc08"
    >
      <div slot="thumbnail-bar" className="thumbnail-bar">
        {items &&
          items.map((item, index) => {
            const isSelected = viewerItemIndex === index;
            const viewer = viewerRef.current;
            const features = item.features || (viewer ? viewer.features : {});
            {
              /* Button where you can pass your own design,
                 or you can call the Content API (CAPI) to get content for each item. */
            }
            return (
              <button
                key={index}
                className={isSelected ? "selected" : ""}
                onClick={() =>
                  (viewer.item = {
                    viewerItem: item,
                    viewerItemIndex: index,
                  })
                }
              >
                {item.type}
              </button>
            );
          })}
      </div>
    </cylindo-viewer>
  );
}

Custom thumbnail bar with different materials' variations

The example below shows a possible approach to creating a custom thumbnail bar with local items with different materials' variations. Clicking on these items will swap the materials' variations and change the current viewer item.

info

This interactive example demonstrates the concept using React. If you're using a different framework, adapt the core concepts to your preferred technology.

Result
Loading...
Live Editor
function Example() {
  const viewerRef = React.useRef(null);
  const [viewerItemIndex, setViewerItemIndex] = useState(0);
  const [localItems, setLocalItems] = useState([]);
  const [configItems, setConfigItems] = useState([]);
  const [viewerFeatures, setViewerFeatures] = useState({});

  useEffect(() => {
    const viewer = viewerRef.current;
    if (!viewer) return;

    const onLocalItemsChange = event => {
      setLocalItems(event.detail);
    };

    const onRemoteItemsChange = event => {
      setConfigItems(event.detail.items);
    };

    const onItemChange = event => {
      setViewerItemIndex(event.detail.index);
    };

    const onFeaturesChange = event => {
      setViewerFeatures(event.detail);
    };

    viewer.addEventListener("items-change", onLocalItemsChange);
    viewer.addEventListener("config-change", onRemoteItemsChange);
    viewer.addEventListener("item-change", onItemChange);
    viewer.addEventListener("features-change", onFeaturesChange);
    return () => {
      viewer.removeEventListener("items-change", onLocalItemsChange);
      viewer.removeEventListener("config-change", onRemoteItemsChange);
      viewer.removeEventListener("item-change", onItemChange);
      viewer.removeEventListener("features-change", onFeaturesChange);
    };
  }, []);

  const items = [
    ...configItems,
    ...localItems,
    // Append the custom items with different materials
    {
      type: "360",
      custom: true,
      features: { UPHOLSTERY: "MONTREAL SAND" },
      frame: 1,
    },
    {
      type: "360",
      custom: true,
      features: { UPHOLSTERY: "VICTORY TEAL" },
      frame: 16,
    },
    {
      type: "360StaticFrame",
      custom: true,
      features: { UPHOLSTERY: "ELEMENT EMERALD" },
      frame: 1,
    }
  ];
  
  const customerId = "5098";
  const code = "WHISTLER SOFA BED";

  return (
    <cylindo-viewer
      ref={viewerRef}
      customer-id={customerId}
      code={code}
      remote-config="k2hctc08"
    >
      <div slot="thumbnail-bar" className="thumbnail-bar">
        {items.map((item, index) => {
            const isSelected = viewerItemIndex === index;
            const features = item.features || viewerFeatures;

            return (
              <button
                key={index}
                className={`thumb ${isSelected ? "selected" : ""}`}
                onClick={() => {
                  const viewer = viewerRef.current;
                  // For our appended items
                  if (item.custom) {
                    // Find the corresponding item
                    const _item = items.find(i => i.type === item.type);
                    // Append the new feature (Material variation)
                    viewer.features = {
                      ...features,
                      ...item.features,
                    };
                    // Set the new item
                    viewer.item = {
                      viewerItem: _item,
                      viewerItemIndex: index,
                    };
                    if (item.frame) {
                      viewer.frame = item.frame;
                    }

                    return;
                  }

                  viewer.item = {
                    viewerItem: item,
                    viewerItemIndex: index,
                  };
                }}
              >
                <img
                  className="thumb-image"
                  src={getThumbnailImgSrc({
                    customerId,
                    code,
                    item,
                    features,
                  })}
                  alt={`Thumbnail bar item - ${item.type}`}
                  draggable="false"
                />
              </button>
            );
          })}
      </div>
    </cylindo-viewer>
  );

  // Get the thumbs images from the Content API
  function getThumbnailImgSrc({ item, customerId, code, features }) {
    const baseUrl = `https://content.cylindo.com/api/v2/${customerId}/products/`;
    // For the sake of this example, we take only the first feature available UPHOLSTERY.
    const featureOption = features["UPHOLSTERY"];
    const featuresParams = featureOption
      ? `&feature=UPHOLSTERY:${encodeURIComponent(featureOption)}`
      : "";
    switch (item.type) {
      case "studio":
        return `${baseUrl}${item.code}/frames/1/${item.code}.webp?size=105${featuresParams}`;
      case "360StaticFrame":
        return `${baseUrl}${code}/frames/${item.frame}/${code}.webp?size=105${featuresParams}`;
      case "360":
        return `${baseUrl}${code}/frames/${
          item.frame || 1
        }/${code}.webp?size=105${featuresParams}`;
      case "model":
        return `${baseUrl}${code}/frames/1/${code}.webp?size=105${featuresParams}`;
      case "dimensionShot":
        return `${baseUrl}${code}/dimensions/${code}.webp?dimensionCode=${item.dimensionCode}&dimensionLabelUnit=${item.dimensionLabelUnit}&size=105${featuresParams}`;
      case "swatch":
        return `${baseUrl}${code}/material/${code}.webp?crop=(32,32,64,64)&size=105&size=105&feature=UPHOLSTERY:${encodeURIComponent(
          featureOption || item.defaultOptionCode
        )}`;
      default:
        throw Error(`Unhandled item type: ${item.type}`);
    }
  }
}

The following code shows the styles used for all the examples above.

::part(thumbnail-bar-fullscreen-container) {
display: flex;
justify-content: center;
align-items: center;
padding: 4px 8px;
}
.thumbnail-bar {
display: flex;
gap: 4px;
display: flex;
column-gap: 10px;
margin: 10px 0;
align-items: center;
padding: 0 1em;
}

.thumb {
cursor: pointer;
padding: 0;
background-color: #abafad26;
border: 1px solid transparent;
border-radius: 8px;
padding: 4px;
box-sizing: content-box;
width: 64px;
height: 48px;
flex-shrink: 0;
transition: border 500ms;
}
.selected {
border: 1px solid #9bb0be;
}
.thumb-image {
display: flex;
justify-content: center;
align-items: center;
object-fit: cover;
width: 100%;
height: 100%;
border-radius: 4px;
}